#![feature(panic_payload_as_str)]
// Without this, the program might run with a terminal opened,
// giving the user the ability to close it even without the knowledge
// of Windows Task Manager.
// #![windows_subsystem = "windows"]

use chrono::{DateTime, Datelike, Local, Timelike};
use serde::{Deserialize, Serialize};
use std::{
    fmt::Debug,
    fs::{File, OpenOptions},
    io::{Result as IoResult, Write},
    sync::LazyLock,
    thread,
    time::Duration,
};
use thread_priority::{set_current_thread_priority, ThreadPriority};
use utils::*;

static CONF: LazyLock<Config> =
    LazyLock::new(|| Config::load_or_write_default().expect("IO Error when loading config!"));
static STATE_FILE: LazyLock<File> = LazyLock::new(|| {
    OpenOptions::new()
        .read(true)
        .write(true)
        .create(true)
        .open("main.state")
        .expect("Unable to open state file!")
});

const DEFAULT_CONF: Config = Config {
    freetime: 60 * 60,
    blocktime: 60 * 60,
    block_action: BlockAction::Sleep,
    block_retry_timeout: 60,
    weekday_daily_freetime: 60 * 60,
    weekend_daily_freetime: 4 * 60 * 60,
};

#[derive(Debug, Serialize, Deserialize)]
enum BlockAction {
    Shutdown,
    Sleep,
}

#[derive(Debug, Serialize, Deserialize)]
struct Config {
    freetime: u64,
    blocktime: u64,
    block_action: BlockAction,
    block_retry_timeout: u64,
    weekday_daily_freetime: u64,
    weekend_daily_freetime: u64,
}

fn with_log_file(f: impl FnOnce(&File) -> IoResult<()>) -> IoResult<()> {
    f(&OpenOptions::new()
        .read(true)
        .append(true)
        .create(true)
        .open("main.log")?)
}

impl Default for Config {
    fn default() -> Self {
        DEFAULT_CONF
    }
}

impl Config {
    fn load_or_write_default() -> IoResult<Self> {
        load_or_write_default(
            &OpenOptions::new()
                .read(true)
                .write(true)
                .create(true)
                .open("main.conf")?,
        )
    }
}

#[derive(Serialize, Deserialize)]
struct UsageStats {
    last_use_time: DateTime<Local>,
    total_freetime: u64,
}

impl UsageStats {
    fn default_with_time(t: DateTime<Local>) -> Self {
        Self {
            last_use_time: t,
            total_freetime: if t.weekday().number_from_monday() < 6 {
                CONF.weekday_daily_freetime
            } else {
                CONF.weekend_daily_freetime
            },
        }
    }
}

impl Default for UsageStats {
    fn default() -> Self {
        Self::default_with_time(Local::now())
    }
}

#[derive(Serialize, Deserialize)]
enum Mode {
    Free { countdown: u64 },
    Blocked { until: DateTime<Local> },
}

impl Default for Mode {
    fn default() -> Self {
        Self::Free {
            countdown: CONF.freetime,
        }
    }
}

#[derive(Default, Serialize, Deserialize)]
struct OmslagState {
    mode: Mode,
    usage_stats: UsageStats,
}

impl OmslagState {
    fn load_or_write_default() -> IoResult<Self> {
        load_or_write_default(&*STATE_FILE)
    }

    fn save_state(&self) -> IoResult<()> {
        store(&*STATE_FILE, self)
    }
}

fn main() {
    std::panic::set_hook(Box::new(|p| {
        if let Some(s) = p.payload_as_str() {
            let _ = with_log_file(|mut f| write!(f, "{}: Exit with {}\n", Local::now(), s));
        }
        loop {
            system_shutdown_now();
            thread::sleep(Duration::from_secs(60));
        }
    }));

    let _ = set_current_thread_priority(ThreadPriority::Max);

    let startup_time = Local::now();

    let _ = with_log_file(|mut f| write!(f, "{}: Start\n", startup_time));

    if !(6..23).contains(&startup_time.hour()) {
        system_suspend()
    }

    let block_retry_timeout = Duration::from_secs(CONF.block_retry_timeout);
    let block_action = match CONF.block_action {
        BlockAction::Shutdown => system_shutdown_now,
        BlockAction::Sleep => system_suspend,
    };

    let mut state =
        OmslagState::load_or_write_default().expect("IO error when initializing volatile state");

    if state.usage_stats.last_use_time.date_naive() < startup_time.date_naive() {
        // New day!
        state.mode = Mode::default();
        state.usage_stats = UsageStats::default_with_time(startup_time);
    } else {
        state.usage_stats.last_use_time = startup_time;
    }

    // one needs to make sure that the replacing year is a leap year so that Feb. 29th exists in
    // that year
    let long_long_after = startup_time.with_year(2400).unwrap();

    loop {
        match state.mode {
            Mode::Free { ref mut countdown } => {
                thread::sleep(Duration::from_secs(1));
                let now = Local::now();

                *countdown -= 1;
                state.usage_stats.total_freetime -= 1;

                if state.usage_stats.total_freetime == 0 || now.hour() > 22 {
                    state.mode = Mode::Blocked {
                        until: long_long_after,
                    };
                } else if *countdown == 0 {
                    state.mode = Mode::Blocked {
                        until: now + Duration::from_secs(CONF.blocktime),
                    };
                }
            }
            Mode::Blocked { ref until } => {
                while Local::now().lt(until) {
                    block_action();
                    thread::sleep(block_retry_timeout);
                }

                let now = Local::now();
                if state.usage_stats.last_use_time.date_naive() < now.date_naive() {
                    // Just woke up in a new day!
                    state.usage_stats = UsageStats::default_with_time(now);
                }

                state.mode = Mode::Free {
                    countdown: CONF.freetime,
                }
            }
        }
        state
            .save_state()
            .expect("IO Error when updating state file!");
    }
}
